package com.instant.searcher;

import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.ansj.domain.Term;
import org.ansj.splitWord.analysis.ToAnalysis;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

//通过这个类在内存中构造索引
public class  Index {

    //放置索引文件的路径
    private static final String INDEX_PATH = "/root/java/project/data/";
    private ObjectMapper objectMapper = new ObjectMapper();

    //使用数组下标表示docId
    private ArrayList<DocInfo> forwardIndex = new ArrayList<>();

    //使用哈希表来表示倒排索引
    private HashMap<String , ArrayList<Weight>> invertedIndex = new HashMap<>();
    //多线程来说如果锁对象都是this那么正排与倒排不能同时进行，但是正排和倒排修改的不是同一内容，所以
    //是可以同时进行的。我们可以设置两个不同的锁对象然后分别上锁
    private Object lock1 = new Object();
    private Object lock2 = new Object();

    //这个类要提供的方法
    //1.给定一个docId，在正排索引中，查询文档的详细信息
    public DocInfo getDocInfo(int docId){
        return forwardIndex.get(docId);
    }
    //2.给定一个词，在倒排索引中，查看哪个文档和这个词有关
    //将和这个词相关的文档id返回，不是很合理。应该按照文档的关联程度做为权重返回
    public List<Weight> getInverted(String term){
        return invertedIndex.get(term);
    }

    //3.往索引中新增一个文档
    public void addDoc(String title, String url , String content){
        //需要同时增加到正排和倒排索引
        //正排索引
        DocInfo docInfo = buildForward(title , url , content);
        //倒排索引
        buildInverted(docInfo);
    }

    //搭建倒排索引
    private void buildInverted(DocInfo docInfo) {
        //词=>文档id之间的映射关系
        //此时就需要对文档的内容（标题和正文）进行分词
        //针对分词结果就可以得到key与文档id之间的对应关系。
        //针对文档进行分词，然后根据每个分词结果去倒排索引中找到对应的value把当前文档id加入到对应的value列表中去
        //此时value中weight包含id和权重两个信息。权重处理比较简单粗暴，就是根据文档出现key的次数进行权重分配
        //而在实际情况中真实的搜索引擎非常复杂，往往由一个团队负责。
        //标题出现关键词权重更大，分别记录标题出现次数以及正文出现次数
        //最终权重 = 标题出现次数 *10 + 正文出现次数
        //遍历刚才的HashMap，依次来进行倒排索引结构。
         class WordCnt{
             //表示这个词在标题中出现次数
             public int titleCount;
             //正文中出现的次数
             public int contentCount;
        }
        //这个数据结构用于统计词频
        HashMap<String , WordCnt> wordCntHashMap = new HashMap<>();

         //分词库会将所有的单词转换为小写，所以就不用自己转换了。一般搜索引擎的结果不区分大小写，所以统一使用小写有利于统计
         List<Term> terms = ToAnalysis.parse(docInfo.getTitle()).getTerms() ;
         for(Term term : terms){
             //先判定一下term是否存在，不存在就创建一个新的键值对，插入禁区，titleCount设为1
             String word = term.getName();
             WordCnt wordCnt = wordCntHashMap.get(word);
             if(wordCnt == null){
                 WordCnt wordCnt1 = new WordCnt();
                 wordCnt1.titleCount = 1;
                 wordCnt1.contentCount = 0;
                 wordCntHashMap.put(word ,wordCnt1);
             }
             //如果存在，就找到之前的值，将titleCount+1
             else{
                 wordCnt.titleCount++;
             }
         }
         //针对正文进行分词
        terms = ToAnalysis.parse(docInfo.getContent()).getTerms() ;
         for(Term term : terms){
             String word = term.getName();
             WordCnt wordCnt = wordCntHashMap.get(word);
             if(wordCnt == null){
                 WordCnt wordCnt1 = new WordCnt();
                 wordCnt1.titleCount = 0;
                 wordCnt1.contentCount = 1;
                 wordCntHashMap.put(word ,wordCnt1);
             }else{
                 wordCnt.contentCount++;
             }
         }
         //遍历上述创建的hashmap依据权重更新到倒排索引中
        for(Map.Entry<String , WordCnt> entry : wordCntHashMap.entrySet()){
            //先根据这里的词到倒排索引中查询
            //倒排拉链 ， 每一篇Doc都需要检查是否存在这个key对应的数列，有就在数列后面加上weight，没有就新建一个数列放进去
            synchronized (lock1){
                ArrayList<Weight> invertedList= invertedIndex.get(entry.getKey());
                if(invertedList == null){
                    //如果为空，就插入一个新的键值对
                    ArrayList<Weight> invertedList1 = new ArrayList<>();
                    //构建DocInfo对象插入
                    Weight weight = new Weight();
                    weight.setDocId(docInfo.getDocId());
                    weight.setWeight(entry.getValue().contentCount + (entry.getValue().titleCount*10));
                    invertedList1.add(weight);
                    invertedIndex.put(entry.getKey() , invertedList1);
                }else{
                    //不为空则构造出Weight对象插入到倒排拉链的后面
                    Weight weight = new Weight();
                    weight.setDocId(docInfo.getDocId());
                    weight.setWeight(entry.getValue().contentCount + (entry.getValue().titleCount*10));
                    invertedList.add(weight);
                }
            }
        }
    }

    //搭建正排索引
    private DocInfo buildForward(String title, String url, String content) {
        DocInfo docInfo = new DocInfo();
        //id=>文档之间的映射关系
        docInfo.setTitle(title);
        docInfo.setUrl(url);
        docInfo.setContent(content);
        synchronized (lock2){
            docInfo.setDocId(forwardIndex.size());
            forwardIndex.add(docInfo);
        }
        return docInfo;
    }

    //4.把内存中的索引结构保存到磁盘中。
    public void save(){
        long beg = System.currentTimeMillis();
        System.out.println("保存索引开始");
        //将两个数据结构保存到文件当中去，使用两个文件进行正排和倒排
        //1.先判断索引对应的目录是否存在，不存在就创建
        File indexFile = new File(INDEX_PATH);
        if(!indexFile.exists()){
            indexFile.mkdirs();
        }
        File forwardIndexFile = new File(INDEX_PATH + "forward.txt");
        File invertedIndexFile = new File(INDEX_PATH + "inverted.txt");
        try{
            objectMapper.writeValue(forwardIndexFile , forwardIndex);
            objectMapper.writeValue(invertedIndexFile , invertedIndex);
        }catch (IOException e){
            e.printStackTrace();
        }
        long end = System.currentTimeMillis();
        System.out.println("保存索引结束" + "消耗时间："+(end - beg));
    }

    //5.把磁盘中索引数据加载到内存中。
    public void load(){
        long beg = System.currentTimeMillis();
        System.out.println("加载索引开始");
        File forwardIndexFile = new File(INDEX_PATH + "forward.txt");
        File invertedIndexFile = new File(INDEX_PATH + "inverted.txt");
        try{
            forwardIndex = objectMapper.readValue(forwardIndexFile,new TypeReference<ArrayList<DocInfo>>(){});
            invertedIndex = objectMapper.readValue(invertedIndexFile , new TypeReference<HashMap<String , ArrayList<Weight>>>(){});
        }catch (IOException e){
            e.printStackTrace();
        }
        long end = System.currentTimeMillis();
        System.out.println("加载索引结束" + "消耗时间"+(end - beg) + "ms");
    }

    public static void main(String[] args) {
        Index index = new Index();
        index.load();
        System.out.println("索引加载完成。");
    }

}
